﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;

namespace GS1.Comunicacion
{
    /// <summary>
    /// <para>Clase       : Servidor</para>
    /// <para>Descripcion : Permite iniciar un servidor via Sockets</para>
    /// <para>Autor       : Pedro Miguel Vera Rafael</para>
    /// <para>Fecha       : 16/05/2011</para>
    /// <para>Empresa     : GS1 Peru</para>
    /// </summary>
    public class Servidor
    {
        #region --- Variables y Propiedades ---
        private AsyncCallback pfnWorkerCallBack;
        private Socket m_mainSocket;
        private Dictionary<string, Socket> listaClientes = null;

        private string dataRecibida = string.Empty;
        private string cadenaTerminador = ";";

        private int cantidadMaximoClientes = 0;
        #endregion

        #region --- Delegados y Eventos ---
        /// <summary>
        /// Delegado: Firma del delegado de Sucesos en el servidor
        /// </summary>
        /// <param name="mensaje">Mensaje o suceso</param>
        public delegate void sucesosServidorHandler(string mensaje);

        /// <summary>
        /// Evento: Que sera ejecutado al ocurrir sucesos en el servidor
        /// </summary>
        public event sucesosServidorHandler sucesosServidor;

        /// <summary>
        /// Delegado: Firma del delegado de Conexion de clientes
        /// </summary>
        /// <param name="mensaje">Data Recibida</param>
        /// <param name="ipClientePuerto">IP y puerto del cliente</param>
        public delegate void conexionClienteHandler(string mensaje, string ipClientePuerto);

        /// <summary>
        /// Evento: Que sera ejecutado al ocurrir conexion de clientes
        /// </summary>
        public event conexionClienteHandler conexionCliente;

        /// <summary>
        /// Delegado: Firma del delegado de Desconexion de clientes
        /// </summary>
        /// <param name="mensaje">Data Recibida</param>
        /// <param name="ipClientePuerto">IP y puerto del cliente</param>
        public delegate void desconexionClienteHanler(string mensaje, string ipClientePuerto);

        /// <summary>
        /// Evento: Que sera ejecutado al ocurrir desconexion de clientes
        /// </summary>
        public event desconexionClienteHanler desconexionCliente;

        /// <summary>
        /// Delegado: Firma del delegado de Recepcion de Data
        /// </summary>
        /// <param name="mensaje">Data recibida</param>
        /// <param name="ipClientePuerto">IP y puerto del cliente</param>
        public delegate void recepcionDataHandler(string mensaje, string ipClientePuerto);
        
        /// <summary>
        /// Evento: Que sera ejecutado al ocurrir recepcion de data en el servidor
        /// </summary>
        public event recepcionDataHandler recepcionData;

        private void OnSucesosServidor(string mensaje)
        {
            if (sucesosServidor != null)
            {
                sucesosServidor(mensaje);
            }
        }

        private void OnConexionCliente(string mensaje, string ipClientePuerto)
        {
            if (conexionCliente != null)
            {
                conexionCliente(mensaje, ipClientePuerto);
            }
        }

        private void OnDesconexionCliente(string mensaje, string ipClientePuerto)
        {
            if (desconexionCliente != null)
            {
                desconexionCliente(mensaje, ipClientePuerto);
            }
        }

        private void OnRecepcionData(string mensaje, string ipClientePuerto)
        {
            if (recepcionData != null)
            {
                recepcionData(mensaje, ipClientePuerto);
            }
        }
        #endregion

        #region --- Constructor ---
        /// <summary>
        /// Metodo: Permite inicializar el objeto
        /// </summary>
        /// <param name="maximoClientes"></param>
        public Servidor(int maximoClientes)
        {
            this.listaClientes = new Dictionary<string, Socket>(maximoClientes);
            this.cantidadMaximoClientes = maximoClientes;
        }
        #endregion

        #region --- Metodos Propios ---
        /// <summary>
        /// Metodo: Permite empezar el servidor con su puerto escucha
        /// </summary>
        /// <param name="puerto">Puerto escucha</param>
        /// <returns>True o False</returns>
        public bool iniciar(int puerto)
        {
            bool resultado = false;

            try
            {
                // Create the listening socket...
                this.m_mainSocket = new Socket(AddressFamily.InterNetwork,
                                          SocketType.Stream,
                                          ProtocolType.Tcp);
                IPEndPoint ipLocal = new IPEndPoint(IPAddress.Any, puerto);
                // Bind to local IP Address...
                this.m_mainSocket.Bind(ipLocal);
                // Start listening...
                this.m_mainSocket.Listen(this.cantidadMaximoClientes);
                // Create the call back for any client connections...
                this.m_mainSocket.BeginAccept(new AsyncCallback(OnClientConnect), null);

                resultado = true;
            }
            catch (SocketException se)
            {
                OnSucesosServidor("Error al conectarse al servidor : " + se.Message);
            }

            return resultado;
        }

        /// <summary>
        /// Metodo: Permite finalizar el servidor
        /// </summary>
        /// <returns></returns>
        public bool detener()
        {
            bool resultado = false;

            if (m_mainSocket != null)
            {
                m_mainSocket.Close();
                resultado = true;
            }

            foreach (var item in this.listaClientes)
            {
                Socket cliente = (Socket)item.Value;
                if (cliente != null)
                {
                    if (cliente.Connected)
                    {
                        cliente.Close();
                        cliente = null;
                    }
                }
            }

            this.listaClientes.Clear();

            return resultado;
        }

        /// <summary>
        /// Metodo: Permite enviar mensajes a los clientes conectados
        /// </summary>
        /// <param name="mensaje">Mensaje a enviarse</param>
        public void enviarMensaje(string mensaje)
        {
            string ipCliente = string.Empty;
            string ipClientePuerto = string.Empty;

            try
            {
                if (mensaje.Equals(string.Empty)) { return; }
                mensaje = agregarTerminador(mensaje);

                byte[] byData = System.Text.Encoding.ASCII.GetBytes(mensaje);

                lock (this)
                {
                    foreach (var item in this.listaClientes)
                    {
                        Socket cliente = (Socket)item.Value;
                        if (cliente != null)
                        {
                            try
                            {
                                if (cliente.Connected)
                                {
                                    ipCliente = ((IPEndPoint)cliente.RemoteEndPoint).Address.ToString();
                                    ipClientePuerto = cliente.RemoteEndPoint.ToString();

                                    cliente.Send(byData);
                                }
                            }
                            catch (SocketException seCliente)
                            {
                                if ((seCliente.SocketErrorCode == SocketError.Disconnecting) ||
                                     (seCliente.SocketErrorCode == SocketError.ConnectionReset))
                                {
                                    // Elimando cliente de lista de conexion
                                    this.listaClientes.Remove(ipCliente);

                                    string datosCliente = string.Format("Desconexion con el cliente IPRemoto {0}", ipClientePuerto);

                                    OnDesconexionCliente(datosCliente, ipClientePuerto);
                                }
                                else
                                {
                                    OnSucesosServidor(seCliente.Message);
                                }
                            }
                        }
                    }
                }

            }
            catch (SocketException se)
            {
                OnSucesosServidor(se.Message);
            }
        }

        private string agregarTerminador(string mensaje)
        {
            string finMensaje = string.Empty;
            string mensajeResultado = string.Empty;

            if (mensaje.Length > 0)
            {
                finMensaje = mensaje.Substring(mensaje.Length - 1, 1);

                if (!finMensaje.Equals(this.cadenaTerminador))
                {
                    mensajeResultado = mensaje + this.cadenaTerminador;
                }
            }

            return mensajeResultado;
        }

        private void desconectarCliente(string ipCliente)
        {
            if (ipCliente.Equals(string.Empty)) { return; }

            Socket cliente = null;
            string ipClientePuerto = string.Empty;
            string datosCliente = string.Empty;

            try
            {
                cliente = (Socket)this.listaClientes[ipCliente];
                if (cliente == null) { return; }

                ipClientePuerto = cliente.RemoteEndPoint.ToString();

                if (cliente.Connected)
                {
                    cliente.Close();
                    cliente = null;
                }

                // Eliminando cliente de lista de conexion
                this.listaClientes.Remove(ipCliente);
                datosCliente = string.Format("Desconexion con el cliente IPRemoto {0}", ipClientePuerto);
                OnDesconexionCliente(datosCliente, ipClientePuerto);
            }
            catch (SocketException seCliente)
            {
                if ((seCliente.SocketErrorCode == SocketError.Disconnecting) ||
                     (seCliente.SocketErrorCode == SocketError.ConnectionReset))
                {
                    // Elimando cliente de lista de conexion
                    this.listaClientes.Remove(ipCliente);
                    datosCliente = string.Format("Desconexion con el cliente IPRemoto {0}", ipClientePuerto);
                    OnDesconexionCliente(datosCliente, ipClientePuerto);
                }
                else
                {
                    OnSucesosServidor(seCliente.Message);
                }
            }

        }
        #endregion

        #region --- Eventos del Servidor ---
        private void OnClientConnect(IAsyncResult asyn)
        {
            try
            {
                Socket clienteEntrante = m_mainSocket.EndAccept(asyn);
                string ipCliente = ((IPEndPoint)clienteEntrante.RemoteEndPoint).Address.ToString();

                if (this.listaClientes.ContainsKey(ipCliente))
                {
                    desconectarCliente(ipCliente);
                }

                // Validar si la supero la maxima cantidad de clientes
                if (this.listaClientes.Count > this.cantidadMaximoClientes)
                {
                    if (clienteEntrante.Connected)
                    {
                        clienteEntrante.Close();
                    }

                    OnSucesosServidor("Se ha superado el maximo de clientes conectados");
                    return;
                }

                this.listaClientes.Add(ipCliente, clienteEntrante);

                WaitForData(clienteEntrante);

                // Display this client connection as a status message on the GUI	
                string datosCliente = string.Format("Cliente conectado IPRemoto {0}", clienteEntrante.RemoteEndPoint);

                OnConexionCliente(datosCliente, clienteEntrante.RemoteEndPoint.ToString());

                // Since the main Socket is now free, it can go back and wait for
                // other clients who are attempting to connect
                m_mainSocket.BeginAccept(new AsyncCallback(OnClientConnect), null);
            }
            catch (ObjectDisposedException)
            {
                OnSucesosServidor("Se detubo el servidor con clientes conectados");
                //OnSucesosServidor("OnClientConnection: Socket has been closed");
            }
            catch (SocketException se)
            {
                OnSucesosServidor(se.Message);
            }

        }

        private class SocketPacket
        {
            public System.Net.Sockets.Socket m_currentSocket;
            public byte[] dataBuffer = new byte[1];
        }

        // Start waiting for data from the client
        private void WaitForData(System.Net.Sockets.Socket soc)
        {
            try
            {
                if (pfnWorkerCallBack == null)
                {
                    // Specify the call back function which is to be 
                    // invoked when there is any write activity by the 
                    // connected client
                    pfnWorkerCallBack = new AsyncCallback(OnDataReceived);
                }
                SocketPacket theSocPkt = new SocketPacket();
                theSocPkt.m_currentSocket = soc;
                // Start receiving any data written by the connected client
                // asynchronously

                soc.BeginReceive(theSocPkt.dataBuffer, 0,
                                   theSocPkt.dataBuffer.Length,
                                   SocketFlags.None,
                                   pfnWorkerCallBack,
                                   theSocPkt);
            }
            catch (SocketException se)
            {
                OnSucesosServidor(se.Message);
            }

        }

        private void OnDataReceived(IAsyncResult asyn)
        {
            string ipCliente = string.Empty;
            string ipClientePuerto = string.Empty;

            try
            {
                SocketPacket socketData = (SocketPacket)asyn.AsyncState;
                // Valida si el cliente aun esta conectado
                if (!socketData.m_currentSocket.Connected) { return; }

                ipCliente = ((IPEndPoint)socketData.m_currentSocket.RemoteEndPoint).Address.ToString();
                ipClientePuerto = socketData.m_currentSocket.RemoteEndPoint.ToString();

                int iRx = 0;
                // Complete the BeginReceive() asynchronous call by EndReceive() method
                // which will return the number of characters written to the stream 
                // by the client
                iRx = socketData.m_currentSocket.EndReceive(asyn);
                char[] chars = new char[iRx + 1];
                System.Text.Decoder d = System.Text.Encoding.UTF8.GetDecoder();
                int charLen = d.GetChars(socketData.dataBuffer,
                                         0, iRx, chars, 0);
                System.String szData = new System.String(chars);

                if (szData.Length > 0)
                {
                    szData = szData.Substring(0, 1);
                }

                dataRecibida += szData;

                if (szData.Equals(cadenaTerminador))
                {
                    OnRecepcionData(dataRecibida, ipClientePuerto);
                    dataRecibida = string.Empty;
                }

                // Continue the waiting for data on the Socket
                WaitForData(socketData.m_currentSocket);
            }
            catch (ObjectDisposedException)
            {
                OnSucesosServidor("OnRecepcionData: Socket has been closed");
            }
            catch (SocketException se)
            {
                if ((se.SocketErrorCode == SocketError.Disconnecting) ||
                     (se.SocketErrorCode == SocketError.ConnectionReset))
                {
                    // Elimando cliente de lista de conexion
                    this.listaClientes.Remove(ipCliente);

                    string datosCliente = string.Format("Desconexion con el cliente IPRemoto {0}", ipClientePuerto);

                    OnDesconexionCliente(datosCliente, ipClientePuerto);
                }
                else
                {
                    OnSucesosServidor(se.Message);
                }
            }
        }
        #endregion
    }
}
